/*
 * Copyright (C) 2012 Matt Broadstone
 * Contact: http://code.google.com/p/qconnman/
 *
 * This file is part of the QConnman Library.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 */

#include "manager.h"
#include "managerinterface.h"

ManagerNode::ManagerNode()
    : m_parent(0),
      m_technology(false)
{
}

ManagerNode::ManagerNode(Technology *technology, ManagerNode *parent)
    : m_parent(parent),
      m_object(technology),
      m_technology(true)
{
}

ManagerNode::ManagerNode(Service *service, ManagerNode *parent)
    : m_parent(parent),
      m_object(service),
      m_technology(false)
{
}

ManagerNode::~ManagerNode()
{
    qDeleteAll(m_children);
}

bool ManagerNode::isTechnology() const
{
    return (!m_object.isNull() && m_technology == true);
}

bool ManagerNode::isService() const
{
    return (!m_object.isNull() && m_technology == false);
}

bool ManagerNode::isRoot() const
{
    return m_object.isNull();
}

QDBusObjectPath ManagerNode::path() const
{
    if (isService()) {
        Service *service = qobject_cast<Service*>(m_object);
        return service->objectPath();
    } else if (isTechnology()) {
        Technology *technology = qobject_cast<Technology*>(m_object);
        return technology->path();
    }

    return QDBusObjectPath();
}

ManagerNode *ManagerNode::parent()
{
    return m_parent;
}

ManagerNode *ManagerNode::child(int index)
{
    if (index >= m_children.size())
        return 0;
    return m_children.at(index);
}

void ManagerNode::appendChild(ManagerNode *node)
{
    node->m_parent = this;
    m_children.append(node);
}

int ManagerNode::childCount() const
{
    return m_children.size();
}

int ManagerNode::childNumber() const
{
    if (m_parent)
        return m_parent->m_children.indexOf(const_cast<ManagerNode*>(this));
    return 0;
}

bool ManagerNode::removeChildren(int position, int count)
{
    if (position < 0 || position + count > m_children.size())
    return false;

    for (int row = 0; row < count; ++row)
        delete m_children.takeAt(position);
    return true;
}




QHash<QString, Manager::State> Manager::s_stateLookup;
Manager::Manager(QObject *parent)
    : QAbstractItemModel(parent),
      m_managerInterface(0),
      m_connmanWatcher(0),
      m_offlineMode(false),
      m_sessionMode(false),
      m_root(0)
{
    // not sure if there is a better place for these guys
    qDBusRegisterMetaType<ObjectPropertyData>();
    qDBusRegisterMetaType<QList<ObjectPropertyData> >();
    qRegisterMetaType<Service*>("Service*");
    qRegisterMetaType<Technology*>("Technology*");

    if (s_stateLookup.isEmpty()) {
        s_stateLookup.insert("offline", Offline);
        s_stateLookup.insert("idle", Idle);
        s_stateLookup.insert("ready", Ready);
        s_stateLookup.insert("online", Online);
    }

    m_root = new ManagerNode;
    m_connmanWatcher = new QDBusServiceWatcher("net.connman",
                                               QDBusConnection::systemBus(),
                                               QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration,
                                               this);
    connect(m_connmanWatcher, SIGNAL(serviceRegistered(QString)), this, SLOT(connmanRegistered()));
    connect(m_connmanWatcher, SIGNAL(serviceUnregistered(QString)), this, SLOT(connmanUnregistered()));

    // we could be starting after connman, so fake the first one
    connmanRegistered();
}

void Manager::connmanRegistered()
{
    qDebug() << Q_FUNC_INFO;
    if (m_managerInterface)
        m_managerInterface->deleteLater();

    m_managerInterface =
        new ManagerInterface("net.connman", "/", QDBusConnection::systemBus(), this);

    if (!m_managerInterface->isValid()) {
        qDebug() << "manager interface is invalid, aborting...";
        return;
    }

    connect(m_managerInterface, SIGNAL(PropertyChanged(QString,QDBusVariant)),
                          this, SLOT(propertyChanged(QString,QDBusVariant)));
    connect(m_managerInterface, SIGNAL(ServicesChanged(QList<ObjectPropertyData>,QList<QDBusObjectPath>)),
                          this, SLOT(servicesChanged(QList<ObjectPropertyData>,QList<QDBusObjectPath>)));

    connect(m_managerInterface, SIGNAL(TechnologyAdded(QDBusObjectPath,QVariantMap)),
                          this, SLOT(technologyAdded(QDBusObjectPath,QVariantMap)));
    connect(m_managerInterface, SIGNAL(TechnologyRemoved(QDBusObjectPath)),
                          this, SLOT(technologyRemoved(QDBusObjectPath)));
    initialize();
}

void Manager::connmanUnregistered()
{
    qDebug() << Q_FUNC_INFO;
    if (m_managerInterface) {
        m_managerInterface->deleteLater();
        m_managerInterface = 0;
    }
}

Manager::~Manager()
{
}

bool Manager::hasService(const QDBusObjectPath &path) const
{
    qDebug() << Q_FUNC_INFO << "path: " << path.path();
    foreach (QDBusObjectPath path, m_services.keys())
        qDebug() << "\t" << path.path();
    return m_services.contains(path);
}

Service *Manager::service(const QDBusObjectPath &path)
{
    return m_services.value(path);
}

Service *Manager::service(const QString &name)
{
    foreach (Service *service, m_services.values()) {
        if(service && service->name() == name)
            return service;
    }
    return 0;
}


Service *Manager::connectedService()
{
    foreach (Service *service, m_services.values()) {
        if(service && service->state() == Service::OnlineState)
            return service;
    }
    return 0;
}

void Manager::disconnectServices()
{
    foreach (Service *service, m_services.values()) {
        if(service && service->state() == Service::OnlineState)
            service->disconnect();
    }
}

bool Manager::hasService(const QString &name) const
{
    foreach (Service *service, m_services.values()) {
        if(service && service->name() == name)
            return true;
    }
    return false;
}

QList<Agent*> Manager::agents() const
{
    return m_agents.values();
}

QList<Technology*> Manager::technologies() const
{
    return m_technologies;
}

QList<Service*> Manager::services() const
{
    qDebug() << "Manager::services(): found" << m_services.values().count() << "services";

    return m_services.values();
}

void Manager::registerAgent(Agent *agent)
{
    QString agentPath = agent->path().path();
    if (agentPath.isEmpty() || agentPath.isNull()) {
        qDebug() << "invalid agent path, aborting...";
        return;
    }

    m_agents.insert(agent->path(), agent);
    QDBusConnection::systemBus().registerObject(agentPath, agent);
    m_managerInterface->RegisterAgent(agent->path());
}

void Manager::unregisterAgent(Agent *agent)
{
    unregisterAgent(agent->path());
}

void Manager::unregisterAgent(const QDBusObjectPath &path)
{
    if (!m_agents.contains(path)) {
        qDebug() << "agent(" << path.path() << ") does not exist, aborting...";
        return;
    }

    m_managerInterface->UnregisterAgent(path);
    QDBusConnection::systemBus().unregisterObject(path.path());
    Agent *agent = m_agents.take(path);
    agent->deleteLater();
}

Manager::State Manager::state() const
{
    return m_state;
}

void Manager::setStateInternal(const QString &state)
{
    if (!s_stateLookup.contains(state)) {
        qDebug() << Q_FUNC_INFO << "invalid state: " << state;
        return;
    }

    m_state = s_stateLookup.value(state);
    Q_EMIT stateChanged();
}

bool Manager::offlineMode() const
{
    return m_offlineMode;
}

void Manager::setOfflineMode(bool offlineMode)
{
    Q_UNUSED(offlineMode)
    // send stuff to connman!
}

void Manager::setOfflineModeInternal(bool offlineMode)
{
    m_offlineMode = offlineMode;
    Q_EMIT offlineModeChanged();
}

bool Manager::sessionMode() const
{
    return m_sessionMode;
}

void Manager::setSessionMode(bool sessionMode)
{
    Q_UNUSED(sessionMode)
    // send stuff to connman!
}

void Manager::setSessionModeInternal(bool sessionMode)
{
    m_sessionMode = sessionMode;
    Q_EMIT sessionModeChanged();
}

void Manager::initialize()
{
    qDebug() << Q_FUNC_INFO;
    // technologies
    QDBusPendingReply<QList<ObjectPropertyData> > tReply = m_managerInterface->GetTechnologies();
    QDBusPendingCallWatcher *tWatcher = new QDBusPendingCallWatcher(tReply, this);
    connect(tWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
                this, SLOT(getTechnologiesResponse(QDBusPendingCallWatcher*)));

    // services
    QDBusPendingReply<QList<ObjectPropertyData> > sReply = m_managerInterface->GetServices();
    QDBusPendingCallWatcher *sWatcher = new QDBusPendingCallWatcher(sReply, this);
    connect(sWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
                this, SLOT(getServicesResponse(QDBusPendingCallWatcher*)));

    // properties
    QDBusPendingReply<QVariantMap> pReply = m_managerInterface->GetProperties();
    QDBusPendingCallWatcher *pWatcher = new QDBusPendingCallWatcher(pReply, this);
    connect(pWatcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
                this, SLOT(getPropertiesResponse(QDBusPendingCallWatcher*)));


    // block (for command line use)
    tWatcher->waitForFinished();
    sWatcher->waitForFinished();
    pWatcher->waitForFinished();
}

void Manager::propertyChanged(const QString &name, const QDBusVariant &value)
{
    setObjectProperty(this, name, value.variant());
}

void Manager::setObjectProperty(QObject *object, const QString &property, const QVariant &value)
{
    int idx = object->metaObject()->indexOfProperty(property.toLatin1());
    if (idx == -1)
        return;

    if (!object->metaObject()->property(idx).write(object, value))
        qDebug() << Q_FUNC_INFO << "could not write property: " << property;
}

void Manager::getTechnologiesResponse(QDBusPendingCallWatcher *call)
{
    QDBusPendingReply<QList<ObjectPropertyData> > reply = *call;
    if (reply.isError()) {
        qDebug() << Q_FUNC_INFO << "error: " << reply.error().message();
    } else {
        QList<ObjectPropertyData> result = reply.value();
        foreach (ObjectPropertyData data, result)
            technologyAdded(data.path, data.properties);
    }

    call->deleteLater();
}

void Manager::getServicesResponse(QDBusPendingCallWatcher *call)
{
    qDebug() << "Manager::getServicesResponse";
    QDBusPendingReply<QList<ObjectPropertyData> > reply = *call;
    if (reply.isError()) {
        qDebug() << Q_FUNC_INFO << "error: " << reply.error().message();
    } else {
        QList<ObjectPropertyData> result = reply.value();
        servicesChanged(result, QList<QDBusObjectPath>());
    }

    call->deleteLater();
}

void Manager::getPropertiesResponse(QDBusPendingCallWatcher *call)
{
    QDBusPendingReply<QVariantMap> reply = *call;
    if (reply.isError()) {
        qDebug() << Q_FUNC_INFO << "error: " << reply.error().message();
    } else {
        QVariantMap result = reply.value();
        foreach (QString property, result.keys())
            setObjectProperty(this, property, result.value(property));
    }

    call->deleteLater();
}

void Manager::servicesChanged(const QList<ObjectPropertyData> &changedServices,
                              const QList<QDBusObjectPath> &removedServices)
{
    qDebug() << Q_FUNC_INFO;
    foreach (ObjectPropertyData data, changedServices) {
        if (nodeForPath(data.path, m_root)) {
            qDebug() << "\tchanged service(" << data.path.path() << ")";
        } else {
            QString technologyType = data.properties.value("Type").toString();
            ManagerNode *parentNode = nodeForTechnologyType(technologyType);
            if (parentNode) {
                Service *service = new Service(data, this);
                QModelIndex parent = index(parentNode->childNumber(), 0, QModelIndex());
                beginInsertRows(parent, parentNode->childCount(), parentNode->childCount() + 1);
                parentNode->appendChild(new ManagerNode(service));
                m_services[service->objectPath()] = service;
                endInsertRows();
                qDebug() << "\tadded service(" << data.path.path() << ")";
            } else {
                qDebug() << "\tinvalid technology specified: " << technologyType;
            }
        }
    }

    foreach (QDBusObjectPath path, removedServices) {
        ManagerNode *serviceNode = nodeForPath(path, m_root);
        if (!serviceNode) {
            qDebug() << "\tcan't remove service(" << path.path() << "), does not exist";
            continue;
        }

        ManagerNode *technologyNode = serviceNode->parent();
        QModelIndex parent = index(technologyNode->childNumber(), 0, QModelIndex());
        beginRemoveRows(parent, serviceNode->childNumber(), serviceNode->childNumber());
        Service *service = serviceNode->object<Service*>();
        service->deleteLater();
        technologyNode->removeChildren(serviceNode->childNumber(), 1);
        endRemoveRows();
        m_services.remove(path);
        qDebug() << "\tremoved service(" << path.path() << ")";
    }
}

void Manager::technologyAdded(const QDBusObjectPath &path, const QVariantMap &properties)
{
    ManagerNode *node = nodeForPath(path, m_root);
    if (node) {
        qDebug() << "attempted addition of known technology(" << path.path() << "), aborting...";
        return;
    }

    beginInsertRows(QModelIndex(), m_root->childCount(), m_root->childCount() + 1);
    Technology *technology = new Technology(path, properties, this);
    m_root->appendChild(new ManagerNode(technology));
    endInsertRows();

    m_technologies.append(technology);

    qDebug() << "added technology(" << path.path() << ")";
}

void Manager::technologyRemoved(const QDBusObjectPath &path)
{
    ManagerNode *node = nodeForPath(path, m_root);
    if (!node) {
        qDebug() << "attempted removal of unknown technology(" << path.path() << "), aborting...";
        return;
    }

    if (!node->isTechnology()) {
        qDebug() << "attempted removal of node which is not a technology, aborting...";
        return;
    }

    beginRemoveRows(QModelIndex(), node->childNumber(), node->childNumber());
    Technology *technology = node->object<Technology*>();
    technology->deleteLater();
    m_root->removeChildren(node->childNumber(), 1);
    endRemoveRows();
    m_technologies.removeAll(technology);
    qDebug() << "removed technology(" << path.path() << ")";
}

ManagerNode *Manager::nodeForIndex(const QModelIndex &index) const
{
    if (index.isValid()) {
        ManagerNode *node = static_cast<ManagerNode*>(index.internalPointer());
        if (node)
            return node;
    }

    return m_root;
}

ManagerNode *Manager::nodeForPath(const QDBusObjectPath &path, ManagerNode *parent) const
{
    for (int i = 0; i < parent->childCount(); i++) {
        ManagerNode *child = parent->child(i);
        if (child->path() == path)
            return child;

        ManagerNode *result = nodeForPath(path, child);
        if (result)
            return result;
    }

    return 0;
}

ManagerNode *Manager::nodeForTechnologyType(const QString &type)
{
    for (int i = 0; i < m_root->childCount(); i++) {
        ManagerNode *node = m_root->child(i);
        Technology *technology = node->object<Technology*>();
        if (technology->type() == type)
            return node;
    }

    return 0;
}


QVariant Manager::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    if (role == Qt::DisplayRole) {
        ManagerNode *node = static_cast<ManagerNode*>(index.internalPointer());
        if ( node->isTechnology() )
            return node->object<Technology*>()->name();
        if ( node->isService() )
            return node->object<Service*>()->name();
        return node->path().path();
    }

    if (role == ManagerNodeRole) {
        return qVariantFromValue(static_cast<ManagerNode*>(index.internalPointer()));
    }

    return QVariant();
}

QModelIndex Manager::index(int row, int column, const QModelIndex &parent) const
{
    if (parent.isValid() && parent.column() != 0)
        return QModelIndex();

    ManagerNode *parentNode = nodeForIndex(parent);
    ManagerNode *childNode = parentNode->child(row);
    if (childNode)
        return createIndex(row, column, childNode);
    return QModelIndex();

}

QModelIndex Manager::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return QModelIndex();

    ManagerNode *childNode = nodeForIndex(index);
    ManagerNode *parentNode = childNode->parent();
    if (parentNode == m_root)
        return QModelIndex();
    return createIndex(parentNode->childNumber(), 0, parentNode);
}

int Manager::rowCount(const QModelIndex &parent) const
{
    ManagerNode *parentNode = nodeForIndex(parent);
    return parentNode->childCount();
}

int Manager::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return 1;
}
